"""服务扫描器模块
用于验证资产有效性，检查端口开放状态，获取服务指纹信息
"""

import socket
import threading
import time
import ssl
import re
from urllib.parse import urlparse, urljoin
import http.client
from concurrent.futures import ThreadPoolExecutor
import logging
from datetime import datetime
from src.core.utils import *

# 配置日志
logging.basicConfig(
    level=logging.INFO,
    format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
)
logger = logging.getLogger('ServiceScanner')

class ServiceScanner:
    """服务扫描器类，用于验证资产有效性，检查端口开放状态，获取服务指纹信息"""
    
    def __init__(self, timeout=3, retry_count=2):
        """初始化服务扫描器
        
        Args:
            timeout: 连接超时时间（秒）
            retry_count: 重试次数
        """
        self.timeout = timeout
        self.retry_count = retry_count
        self.user_agent = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36"

        # 指纹特征库
        self.fingerprints = {
            # Web Servers
            'http': [r'HTTP/[\d.]+', r'X-Powered-By: PHP/', r'X-Powered-By: ASP.NET'],
            'https': [r'HTTPS'],
            'nginx': [r'nginx/[\d.]+', r'Server: nginx'],
            'apache': [r'Apache/[\d.]+', r'Server: Apache', r'Apache[\d\.]* \(', r'Powered by Apache'],
            'iis': [r'Microsoft-IIS/[\d.]+', r'Server: Microsoft-IIS'],
            'tomcat': [r'Apache Tomcat/[\d.]+', r'Server: Apache-Coyote/'],
            'lighttpd': [r'lighttpd/[\d.]+', r'Server: lighttpd'],
            'caddy': [r'Caddy', r'Server: Caddy'],
            
            # Databases
            'mysql': [r'[mM][yY][sS][qQ][lL]', r'mysql_', r'MySQL'],
            'postgresql': [r'PostgreSQL', r'postgresql', r'libpq'],
            'mongodb': [r'mongodb', r'MongoDB'],
            'redis': [r'redis', r'Redis'],
            'sqlite': [r'SQLite', r'sqlite'],
            'oracle': [r'Oracle', r'Oracle Database'],
            'mssql': [r'SQL Server', r'Microsoft SQL Server', r'ODBC Driver.*SQL Server'],
            
            # Programming Languages / Frameworks
            'php': [r'PHP/[\d.]+', r'X-Powered-By: PHP/', r'PHPSESSID'],
            'python': [r'Python/[\d.]+', r'Server: WSGIServer/', r'Python-urllib'],
            'nodejs': [r'Node.js', r'Express', r'X-Powered-By: Express'],
            'java': [r'Java/', r'JSP/', r'Servlet/'],
            'ruby': [r'Ruby', r'Rails', r'Phusion Passenger'],
            'asp_net': [r'ASP.NET', r'X-AspNet-Version', r'X-AspNetMvc-Version'],
            'go': [r'Go', r'golang'],
            
            # File Transfer & Sharing
            'ftp': [r'220.*FTP', r'FTP server ready'],  # FTP服务器欢迎标语[3,9](@ref)
            'sftp': [r'SSH-2.0-', r'SFTP'],  # SFTP基于SSH[3,9](@ref)
            'smb': [r'SMB', r'\xfeSMB', r'NT LM 0.[12]'],  # SMB协议签名[4](@ref)
            'tftp': [r'TFTP', r'Read|Write'],  # TFTP操作码[4](@ref)
            
            # Mail Protocols (明文或TLS握手阶段)
            'smtp': [r'220.*SMTP', r'250-STARTTLS', r'EHLO', r'HELO'],  # SMTP欢迎和命令[1,12](@ref)
            'pop3': [r'\+OK.*POP3', r'USER', r'PASS'],  # POP3响应和命令[1](@ref)
            'imap': [r'\* OK.*IMAP', r'LOGIN', r'SELECT'],  # IMAP响应和命令[1](@ref)
            'smtps': [r'220.*ESMTP', r'250-AUTH'],  # 加密SMTP[1](@ref)
            'pop3s': [r'\+OK.*POP3', r'STLS'],  # 加密POP3[1](@ref)
            'imaps': [r'\* OK.*IMAP', r'STARTTLS'],  # 加密IMAP[1](@ref)
            
            # Remote Access & Administration
            'ssh': [r'SSH-([\d\.]+)-', r'ssh-(dss|rsa)'],  # SSH版本和密钥类型[6,7](@ref)
            'rdp': [r'\x03\x00\x00.*', r'Cookie: mstshash='],  # RDP协议前缀和Cookie[4](@ref)
            'telnet': [r'\xff\xfb\x01\xff\xfb\x03', r'Login:'],  # Telnet选项协商和登录提示[4](@ref)
            'vnc': [r'RFB [\d\.]+', r'VNC'],  # VNC协议标识
            
            # Network Services
            'dns': [r'\x00[\x01-\x10]', r'[\x80-\x8f]\x00'],  # DNS查询/响应头2字节（ID+Flags）[3](@ref)
            'dhcp': [r'\x01[\x01-\x06]', r'DHCP'],  # DHCP消息类型（Discover/Offer/Request/ACK等）[3](@ref)
            'ntp': [r'\x1b\x00\x00\x00\x00\x00\x00\x00', r'NTP'],  # NTP v4客户端请求（LI=3, Mode=3）[4](@ref)
            'snmp': [r'\x30[\x80-\xff]', r'public|private'],  # SNMP序列号和常见社区字符串[4](@ref)
            'ldap': [r'0\x84.*\x02\x01', r'LDAP'],  # LDAP BER序列（Tag=0x30, Length, MessageID=1）[4](@ref)
            
            # Industrial Protocols (基于专利和RFC的常见特征)
            'modbus_tcp': [r'\x00\x01.*\x00\x00', r'Modbus'],  # MBAP Transaction ID + Protocol ID
            'ethercat': [r'\x88\xA4'],  # EtherType 0x88A4
            'can_bus': [r'CAN', r'CANbus'],
            
            # VPN & Encryption
            'openvpn': [r'\x38\x01.*\x00\x00', r'OpenVPN'],  # OpenVPN控制信道P\_ACK\_V1消息
            'ipsec': [r'IKEv[12]', r'ISAKMP'],  # IKE版本和ISAKMP标识
            'pptp': [r'\x00\x01.*\x00\x00', r'PPTP'],  # PPTP控制消息（StartCtrlConnRequest等）
            
            # Middleware & Other Services
            'wordpress': [r'wp-content', r'wp-includes', r'WordPress'],
            'drupal': [r'Drupal', r'drupal'],
            'joomla': [r'Joomla!', r'joomla'],
            'elasticsearch': [r'elasticsearch', r'Elasticsearch'],
            'docker': [r'Docker', r'docker'],
            'kubernetes': [r'Kubernetes'],

            'mqtt': [
                r'MQTT',  # 协议名
                r'CONNACK',  # 连接确认包
                r'PUBLISH',  # 发布包
                r'SUBSCRIBE',  # 订阅包
            ],
            'mosquitto': [
                r'mosquitto/[\d.]+',  # Mosquitto 版本
            ],
            'emqx': [
                r'EMQX/[\d.]+',  # EMQX 版本
            ],
            'hivemq': [
                r'HiveMQ/[\d.]+',  # HiveMQ 版本
            ],
            # 阿里云 IoT 平台等厂商通常会有特定主题或证书信息
            'aliyun_iot': [
                r'\/sys\/[^\/]+\/[^\/]+\/thing\/event\/property\/post',  # 阿里云物模型属性上报主题
                r'productKey',  # 阿里云三元组之一
                r'deviceName',  # 阿里云三元组之一
            ]
        }

        # 横幅
        self.common_probes = {
            # 文件传输协议
            'ftp': b'USER anonymous\r\n',  # 匿名用户查询，绝大多数FTP服务器会响应
            'sftp': b'\x00\x00\x00\x04\xff\xff\xff\xff',  # SSH_FXP_INIT (SFTP v3)，但SFTP通常在SSH会话内，此探针可能无效，更依赖SSH层
            'tftp': b'\x00\x01\x66\x69\x6c\x65\x6e\x61\x6d\x65\x00\x6e\x65\x74\x61\x73\x63\x69\x69\x00',  # RRQ for "filename" in netascii mode (TFTP Read Request)

            # 邮件协议
            'smtp': b'EHLO example.com\r\n',  # SMTP的EHLO命令，用于查询服务器支持的功能
            'smtps': b'EHLO example.com\r\n',  # 加密的SMTP，同样先发送EHLO
            'pop3': b'USER anonymous\r\n',     # POP3的用户命令
            'pop3s': b'USER anonymous\r\n',    # 加密的POP3
            'imap': b'1 LOGIN anonymous anonymous\r\n',  # IMAP的登录命令
            'imaps': b'1 LOGIN anonymous anonymous\r\n', # 加密的IMAP

            # 远程访问与管理
            'ssh': b'SSH-2.0-OpenSSH_8.4\r\n',  # SSH客户端版本标识（虽然服务器通常先发送，但某些配置或自定义实现可能需要客户端先发）
            'telnet': b'\xff\xfb\x01\xff\xfb\x03\xff\xfd\x18',  # Telnet协商指令（IAC DO ECHO, IAC DO Suppress Go Ahead, IAC WILL Terminal Type）
            'rlogin': b'\x00',  # 传统的rlogin协议可能以空字节开始，但非常不常见且不安全

            # 数据库协议
            'mysql': b'\x85\xa6\x03\x00',  # 示例：MySQL握手初始包的一部分（长度、序列号、协议版本），但实际握手更复杂
            # 更常见的MySQL探测是等待服务器发送初始握手包，或者发送一个简单的查询如 `SELECT 1` (但需要先完成握手)
            'postgresql': b'\x00\x00\x00\x08\x04\xd2\x16\x2f',  # PostgreSQL启动消息（长度8，协议版本1234.5678的魔术数不准确，实际是SSL请求或取消消息更常见）
            # 更常见的PostgreSQL探测是发送SSL请求或启动消息 `\x00\x00\x00\x08\x04\xd2\x16\x2f` 实际上是取消请求的魔术数字
            'mongodb': b'\x3f\x00\x00\x00\x00\x00\x00\x00\xd4\x07\x00\x00\x00\x00\x00\x00admin.$cmd\x00\x00\x00\x00\x00\xff\xff\xff\xff\x13\x00\x00\x00\x10ismaster\x00\x01\x00',  # MongoDB ISMASTER命令（OP_QUERY)
            'redis': b'*1\r\n$4\r\nPING\r\n',  # Redis PING 命令
            'mssql': b'\x12\x01\x00\x34\x00\x00\x00\x00\x00\x00\x15\x00\x06\x01\x00\x1b\x00\x01\x02\x00\x1c\x00\x0c\x03\x00\x28\x00\x04\xff\x08\x00\x01\x55\x00\x00\x00\x4d\x53\x53\x51\x4c\x53\x65\x72\x76\x65\x72\x00',  # TDS Pre-Login Message

            # 网络服务与工业协议
            'dns': b'\xaa\xaa\x01\x00\x00\x01\x00\x00\x00\x00\x00\x00\x07\x65\x78\x61\x6d\x70\x6c\x65\x03\x63\x6f\x6d\x00\x00\x01\x00\x01',  # DNS查询 (transaction ID 0xaaaa, 查询 example.com A记录)
            'snmp': b'\x30\x29\x02\x01\x00\x04\x06\x70\x75\x62\x6c\x69\x63\xa0\x1c\x02\x04\x6b\x2b\x1a\x3d\x02\x01\x00\x02\x01\x00\x30\x0e\x30\x0c\x06\x08\x2b\x06\x01\x02\x01\x01\x01\x00\x05\x00',  # SNMP GetRequest (v1, community "public", sysDescr.0)
            'ntp': b'\xe3\x00\x04\xfa\x00\x01\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00',  # NTP v4 (LI=3, Mode=3 - Client), 不完全标准
            'modbus_tcp': b'\x00\x01\x00\x00\x00\x06\x01\x03\x00\x00\x00\x01',  # MBAP + Modbus PDU (Transaction ID 1, Unit ID 1, Read Holding Registers at address 0, quantity 1)
            'bacnet': b'\x81\x0b\x00\x0c\x01\x20\xff\xff\x00\xff\x10\x00',      # BACnet Virtual Link Control (BVLC) Who-Is request (可能需要根据网络调整)
            'sip': b'OPTIONS sip:example.com SIP/2.0\r\nVia: SIP/2.0/UDP 192.168.1.2:5060;branch=z9hG4bK743asds\r\nFrom: <sip:probe@example.com>;tag=12345\r\nTo: <sip:server@example.com>\r\nCall-ID: 1234567890@192.168.1.2\r\nCSeq: 1 OPTIONS\r\nMax-Forwards: 70\r\nContent-Length: 0\r\n\r\n',  # SIP OPTIONS 请求
            'ldap': b'0\x1d\x02\x01\x01`\x18\x02\x01\x03\x04\x00\x80\x16\x31\x2e\x33\x2e\x36\x2e\x31\x2e\x34\x2e\x31\x2e\x31\x34\x36\x36\x2e\x32\x30\x30\x33\x37',  # LDAP Bind Request (Simple, null DN, null password) + RootDSE查询的BER编码（不完全标准，有时直接发送StartTLS或特定查询）

            # Web服务与中间件
            'http': b'GET / HTTP/1.0\r\nHost: example.com\r\n\r\n',  # 基本的HTTP GET请求，用于获取服务器横幅
            'https': b'GET / HTTP/1.0\r\nHost: example.com\r\n\r\n', # 同样发送HTTP请求，但通过SSL/TLS隧道
            'http2': b'PRI * HTTP/2.0\r\n\r\nSM\r\n\r\n',  # HTTP/2连接前言 (Client sends first)
            'html': b'GET / HTTP/1.0\r\nHost: example.com\r\n\r\n', # 同HTTP，获取HTML响应
            'soap': b'POST / HTTP/1.1\r\nHost: example.com\r\nContent-Type: text/xml; charset=utf-8\r\nContent-Length: 100\r\n\r\n<?xml version="1.0"?><soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/"><soap:Body></soap:Body></soap:Envelope>',  # 简化的SOAP请求（需要根据实际服务调整Action和Body）
            'xmlrpc': b'POST /RPC2 HTTP/1.0\r\nHost: example.com\r\nContent-Type: text/xml\r\nContent-Length: 200\r\n\r\n<?xml version="1.0"?><methodCall><methodName>system.listMethods</methodName><params></params></methodCall>',  # XML-RPC 系统方法查询
            'jsonrpc': b'POST / HTTP/1.1\r\nHost: example.com\r\nContent-Type: application/json\r\nContent-Length: 50\r\n\r\n{"jsonrpc": "2.0", "method": "system.describe", "id": 1}',  # JSON-RPC 2.0 系统描述请求

            'mqtt': b'\x10\x00\x00\x04MQTT\x04\x00\x00\x00\x00\x00',

            # 其他或通用
            'echo': b'echo test\r\n',  # 发送到echo端口(7)的文本
            'discard': b'discard test\r\n', # 发送到discard端口(9)的文本
            'daytime': b'', #  daytime服务(13)通常主动返回日期时间
            'time': b'',    # time服务(37)通常返回32位二进制时间（从1900年1月1日开始的秒数）
            'netbios': b'\x80\xf0\x00\x10\x00\x01\x00\x00\x00\x00\x00\x00\x20\x43\x4b\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x00\x00\x21\x00\x01',  # NetBIOS Name Query Request
            'upnp': b'M-SEARCH * HTTP/1.1\r\nHost: 239.255.255.250:1900\r\nMan: "ssdp:discover"\r\nMX: 3\r\nST: ssdp:all\r\n\r\n',  # UPnP SSDP M-SEARCH 发现消息
            'coap': b'\x40\x01\x00\x00\xb1\x62\x74\x65\x73\x74',  # CoAP GET request (Ver=1, Type=0 (Con), Code=1 (GET), Message ID=0, Token="b", Uri-Path="test")
        }
    
    def check_port_open(self, host, port, protocol='tcp'):
        """检查指定主机的端口是否开放
        
        Args:
            host: 主机名或IP地址
            port: 端口号
            protocol: 协议类型（tcp或udp）
            
        Returns:
            bool: 端口是否开放
        """
        for i in range(2):
            if protocol == 'udp':
                if check_udp_port(host, port):
                    return True
            elif protocol == 'tcp':
                if check_tcp_port(host, port):
                    return True
            else:
                return False
            time.sleep(0.1)
        return False
    
    def scan_service(self, asset):
        """扫描单个资产的服务信息
        
        Args:
            asset: 资产字典，包含ip/domain, port, protocol等信息
            
        Returns:
            dict: 包含扫描结果的字典
        """
        result = {
            'asset_id': asset.get('asset_id'),
            'ip': asset.get('ip'),
            'domain': asset.get('domain'),
            'port': asset.get('port'),
            'protocol': asset.get('asset_protocol', 'tcp'),
            'is_open': False,
            'is_valid': False,
            'asset_service': None,
            'fingerprint': None,
            'title': None,
            'response': None,
            'status_code': None,
            'scan_time': datetime.now().strftime('%Y-%m-%d %H:%M:%S')
        }
        
        # 确定要扫描的主机
        host = result['domain'] if result['domain'] else result['ip']
        if not host:
            logger.error(f"资产 {result['asset_id']} 缺少IP或域名信息")
            return result
        
        # 检查主机是否在线（没必要检测是否在线了）
        # if not is_ip_online(host, timeout=self.timeout):
        #     logger.info(f"主机 {host} 不在线")
        #     return result
        
        # 检查端口是否开放
        result['is_open'] = self.check_port_open(host, result['port'], result['protocol'])
        if not result['is_open']:
            logger.info(f"主机 {host} 的端口 {result['port']} 未开放")
            return result
        
        # 根据端口和协议尝试获取服务信息
        if result['protocol'].lower() == 'tcp':
            # 尝试识别是否为HTTP/HTTPS服务
            is_http_service = self._check_http_service(host, result['port'])
            if is_http_service:
                # 是HTTP/HTTPS服务，获取详细信息
                http_result = self._scan_http_service(host, result['port'])
                result.update(http_result)
                result['is_valid'] = True
            else:
                # 尝试获取其他TCP服务的指纹
                banner = self._grab_banner(host, result['port'])
                if banner:
                    result['response'] = banner
                    result['fingerprint'] = self._identify_fingerprint(banner)
                    result['is_valid'] = True
        
        logger.info(f"资产 {result['asset_id']} 扫描完成: 端口{'开放' if result['is_open'] else '关闭'}, 资产{'有效' if result['is_valid'] else '无效'}")
        return result
    
    def scan_assets(self, assets, threads=10):
        """批量扫描资产
        
        Args:
            assets: 资产列表
            threads: 线程数
            
        Returns:
            list: 扫描结果列表
        """

        logger.info(f"开始检查 {len(assets)} 个资产...")

        results = []
        
        with ThreadPoolExecutor(max_workers=threads) as executor:
            # 提交所有扫描任务
            future_to_asset = {executor.submit(self.scan_service, asset): asset for asset in assets}
            
            # 收集结果
            for future in future_to_asset:
                asset = future_to_asset[future]
                try:
                    result = future.result()
                    results.append(result)
                except Exception as e:
                    logger.error(f"扫描资产 {asset.get('asset_id')} 时发生异常: {str(e)}")
                    # 添加一个错误结果
                    error_result = {
                        'asset_id': asset.get('asset_id'),
                        'ip': asset.get('ip'),
                        'domain': asset.get('domain'),
                        'port': asset.get('port'),
                        'protocol': asset.get('asset_protocol', 'tcp'),
                        'is_open': False,
                        'is_valid': False,
                        'error': str(e),
                        'scan_time': datetime.now().strftime('%Y-%m-%d %H:%M:%S')
                    }
                    results.append(error_result)
        
        return results
        
    def scan_assets_one_by_one(self, assets, db_connector, threads=10):
        """多线程扫描资产，每检测完一个就更新和打印一个
        
        Args:
            assets: 资产列表
            db_connector: 数据库连接器实例
            threads: 线程数
        
        Returns:
            tuple: (总扫描数, 有效资产数, 端口开放数, 更新数)
        """
        
        logger.info(f"开始检查 {len(assets)} 个资产...")
        
        # 用于统计
        total_count = len(assets)
        valid_count = 0
        open_count = 0
        update_count = 0
        
        # 创建锁，避免多线程同时打印导致输出混乱
        print_lock = threading.Lock()
        
        with ThreadPoolExecutor(max_workers=threads) as executor:
            # 提交所有扫描任务
            future_to_asset = {executor.submit(self.scan_service, asset): asset for asset in assets}
            
            # 处理完成的任务，每完成一个就立即更新和打印
            for future in future_to_asset:
                asset = future_to_asset[future]
                try:
                    # 获取扫描结果
                    result = future.result()
                    
                    # 统计结果
                    if result['is_valid']:
                        valid_count += 1
                    if result['is_open']:
                        open_count += 1
                    
                    # 更新数据库
                    try:
                        self.update_asset_with_scan_result(asset, result, db_connector)
                        update_count += 1
                    except Exception as e:
                        logger.error(f"更新资产 {asset.get('asset_id')} 信息时发生错误: {str(e)}")
                    
                    # 打印结果
                    with print_lock:
                        status = "有效" if result['is_valid'] else ("端口开放" if result['is_open'] else "无效")
                        host = result.get('domain') or result.get('ip') or "未知"
                        print(f"资产ID: {result['asset_id']}, 主机: {host}, 端口: {result['port']}, 状态: {status}")
                        if result.get('fingerprint'):
                            print(f"  指纹: {result['fingerprint']}")
                        if result.get('title'):
                            print(f"  标题: {result['title']}")
                        if result.get('status_code') is not None:
                            print(f"  HTTP状态码: {result['status_code']}")
                        print()
                except Exception as e:
                    # 打印错误信息
                    with print_lock:
                        logger.error(f"扫描资产 {asset.get('asset_id')} 时发生异常: {str(e)}")
        
        return total_count, valid_count, open_count, update_count
    
    def _check_http_service(self, host, port):
        """检查指定端口是否为HTTP服务
        
        Args:
            host: 主机名或IP地址
            port: 端口号
            
        Returns:
            bool: 是否为HTTP服务
        """
        for scheme in ['https', 'http']:
            try:
                url = f"{scheme}://{host}:{port}"
                conn = None
                
                if scheme == 'https':
                    context = ssl.create_default_context()
                    context.check_hostname = False
                    context.verify_mode = ssl.CERT_NONE
                    conn = http.client.HTTPSConnection(host, port, context=context, timeout=self.timeout)
                else:
                    conn = http.client.HTTPConnection(host, port, timeout=self.timeout)
                
                conn.request('HEAD', '/', headers={'User-Agent': self.user_agent})
                response = conn.getresponse()
                conn.close()
                
                # 如果返回了HTTP状态码，认为是HTTP服务
                return 100 <= response.status < 600
            except Exception:
                continue
        
        return False
    
    def _scan_http_service(self, host, port):
        """扫描HTTP/HTTPS服务的详细信息
        
        Args:
            host: 主机名或IP地址
            port: 端口号
            
        Returns:
            dict: 包含HTTP服务信息的字典
        """
        result = {
            'asset_service': 'http',
            'fingerprint': None,
            'title': None,
            'response': None,
            'status_code': None
        }
        
        # 尝试HTTP和HTTPS
        for scheme in ['https', 'http']:
            try:
                url = f"{scheme}://{host}:{port}"
                conn = None
                
                if scheme == 'https':
                    context = ssl.create_default_context()
                    context.check_hostname = False
                    context.verify_mode = ssl.CERT_NONE
                    conn = http.client.HTTPSConnection(host, port, context=context, timeout=self.timeout)
                else:
                    conn = http.client.HTTPConnection(host, port, timeout=self.timeout)
                
                conn.request('GET', '/', headers={'User-Agent': self.user_agent})
                response = conn.getresponse()
                
                # 获取状态码
                result['status_code'] = response.status
                
                # 获取响应头和响应体
                headers = '\n'.join([f"{k}: {v}" for k, v in response.getheaders()])
                body = response.read(4096).decode('utf-8', errors='ignore')  # 只读取部分响应体
                
                # 组合响应信息
                result['asset_service'] = scheme
                result['response'] = f"{scheme.upper()}/{response.version} {response.status} {response.reason}\n{headers}\n\n{body}"
                
                # 识别指纹
                fingerprint = self._identify_fingerprint(result['response'])
                result['fingerprint'] = fingerprint
                
                # 提取标题
                title_match = re.search(r'<title>(.*?)</title>', body, re.IGNORECASE)
                if title_match:
                    result['title'] = title_match.group(1).strip()
                
                conn.close()
                break  # 成功获取信息后退出循环
            except Exception as e:
                logger.debug(f"尝试{scheme}连接 {host}:{port} 时发生错误: {str(e)}")
                continue
        
        return result
    
    def _grab_banner(self, host, port):
        """获取服务横幅信息
        
        Args:
            host: 主机名或IP地址
            port: 端口号
            
        Returns:
            str: 横幅信息
        """
        banner = ''
        
        try:
            with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
                s.settimeout(self.timeout)
                s.connect((host, port))
                
                # 尝试发送探测数据
                for probe_data in self.common_probes.values():
                    try:
                        if probe_data:
                            s.send(probe_data)
                        
                        # 接收响应
                        try:
                            response = s.recv(1024).decode('utf-8', errors='ignore')
                            if response:
                                banner += response
                                break
                        except socket.timeout:
                            continue
                    except Exception:
                        continue
                
                # 如果没有获取到横幅，尝试读取可能已有的数据
                if not banner:
                    try:
                        s.settimeout(0.5)  # 更短的超时
                        response = s.recv(1024).decode('utf-8', errors='ignore')
                        banner = response
                    except Exception:
                        pass
        except Exception as e:
            logger.error(f"获取横幅 {host}:{port} 时发生错误: {str(e)}")
        
        return banner.strip()
    
    def _identify_fingerprint(self, content):
        """根据内容识别服务指纹
        
        Args:
            content: 包含服务信息的字符串
            
        Returns:
            str: 识别出的指纹，多个指纹用逗号分隔
        """
        identified_fingerprints = []
        
        for service, patterns in self.fingerprints.items():
            for pattern in patterns:
                if re.search(pattern, content): # 要区分大小写,不然指纹太容易错误了
                    if service not in identified_fingerprints:
                        identified_fingerprints.append(service)
                    break
        
        return ','.join(identified_fingerprints)
    
    def update_asset_with_scan_result(self, asset, scan_result, db_connector):
        """使用扫描结果更新数据库中的资产信息
        
        Args:
            asset: 资产字典
            scan_result: 扫描结果字典
            db_connector: 数据库连接器实例
        """
        try:
            # 检查资产ID是否为临时资产ID，如果是则不更新数据库
            if asset.get('asset_id') == 'temp_url_asset':
                return
            
            # 准备要更新的字段
            fields = {
                'is_valid': scan_result['is_valid'],
                'last_check_time': scan_result['scan_time']
            }
            
            # 添加有值的字段
            if scan_result['is_valid']:
                fields['asset_service'] = scan_result['asset_service']
                fields['asset_fingerprint'] = scan_result['fingerprint']
                fields['asset_title'] = scan_result['title']
                fields['status_code'] = scan_result['status_code']
                fields['asset_response'] = scan_result['response']
            
            # 构建SQL更新语句
            set_clause = ', '.join([f"{k} = %s" for k in fields.keys()])
            sql = f"UPDATE assets SET {set_clause} WHERE asset_id = %s"
            
            # 准备参数
            params = list(fields.values()) + [asset['asset_id']]
            
            # 执行更新
            db_connector.execute_query(sql, params)
            
            # 如果有响应内容，保存到单独的表或日志
            if scan_result.get('response') and len(scan_result['response']) > 0:
                # 这里可以选择保存到单独的表，或者只记录日志
                logger.debug(f"资产 {asset['asset_id']} 的响应内容已记录")
                
        except Exception as e:
            logger.error(f"更新资产 {asset['asset_id']} 信息时发生错误: {str(e)}")
            # 不抛出异常，继续处理其他资产